#region CVS Log
/*
 * Version:
 *   $Id: EncodedString.cs,v 1.8 2004/11/20 23:09:05 cwoodbury Exp $
 *
 * Revisions:
 *   $Log: EncodedString.cs,v $
 *   Revision 1.8  2004/11/20 23:09:05  cwoodbury
 *   Fixed bug #1067703: ISO-8859-1 not correctly implemented.
 *   Added default constructor.
 *
 *   Revision 1.7  2004/11/19 18:38:47  cwoodbury
 *   Added code to catch and handle malformed data.
 *
 *   Revision 1.6  2004/11/16 06:44:22  cwoodbury
 *   Refactored code.
 *
 *   Revision 1.4  2004/11/10 06:51:56  cwoodbury
 *   Hid CVS log messages away in #region
 *
 *   Revision 1.3  2004/11/10 06:31:14  cwoodbury
 *   Updated documentation.
 *
 *   Revision 1.2  2004/11/10 04:44:16  cwoodbury
 *   Updated documentation.
 *
 *   Revision 1.1  2004/11/03 01:18:26  cwoodbury
 *   Added to ID3Sharp
 *
 */
#endregion

/* 
 * Author(s):
 *   Chris Woodbury 
 * 
 * Project Location:
 *	 http://id3sharp.sourceforge.net
 * 
 * License:
 *   Licensed under the Open Software License version 2.0
 */

using System;
using System.Text;
using System.Collections;

namespace ID3Sharp.IO
{
	/// <summary>
	/// A string encoded in any of a number of encodings.
	/// </summary>
	public class EncodedString
	{
		#region Static Encoding objects
		/// <summary>A static UnicodeEncoding instance for
		/// BOM-prefixed Unicode strings.</summary>
		private static Encoding utf16Encoding = new UnicodeEncoding( true, true );
		/// <summary>A static UnicodeEncoding instance for
		/// Big-Endian Unicode strings.</summary>
		private static Encoding utf16BEEncoding = new UnicodeEncoding( true, false );
		/// <summary>A static UTF8Encoding instance.</summary>
		private static Encoding utf8Encoding = new UTF8Encoding();
		#endregion

		#region Fields
		/// <summary>The TextEncodingType of the string.</summary>
		private TextEncodingType encodingType;
		/// <summary>The string.</summary>
		private string str;
		#endregion

		#region Constructors
		/// <summary>
		/// Creates a new empty EncodedString with the default text encoding.
		/// </summary>
		public EncodedString()
		{
			this.TextEncodingType = TextEncodingType.ISO_8859_1;
			this.String = "";
		}

		/// <summary>
		/// Creates a new EncodedString.
		/// </summary>
		/// <param name="encodingType">The type of encoding to use.</param>
		/// <param name="str">The string from which to create the EncodedString.</param>
		public EncodedString( TextEncodingType encodingType, string str )
		{
			this.TextEncodingType = encodingType;
			this.String = str;
		}

		/// <summary>
		/// Creates a new EncodedString.
		/// </summary>
		/// <param name="encodingType">The type of encoding to use.</param>
		/// <param name="stringBytes">The byte array from which to create the EncodedString.</param>
		public EncodedString( TextEncodingType encodingType, byte[] stringBytes )
		{
			this.encodingType = encodingType;
			
			switch( encodingType )
			{
				case TextEncodingType.ISO_8859_1:
					this.ISO_8859_1Bytes = stringBytes;
					break;
				case TextEncodingType.UTF_16:
					this.UTF_16Bytes = stringBytes;
					break;
				case TextEncodingType.UTF_16BE:
					this.UTF_16BEBytes = stringBytes;
					break;
				case TextEncodingType.UTF_8:
					this.UTF_8Bytes = stringBytes;
					break;
			}
		}
		#endregion

		#region Factory methods
		/// <summary>
		/// Parses a byte array and creates and returns one or more EncodedStrings
		/// from the byte array. NULL bytes are interpreted as string terminators.
		/// </summary>
		/// <param name="stringDataWithEncoding">A byte-array string whose first byte
		/// is a text encoding field.</param>
		/// <returns>One or more EncodedStrings created from the byte array.</returns>
		public static ArrayList CreateEncodedStrings( byte[] stringDataWithEncoding )
		{
			// .Length - 1: first byte of string is encoding, not string data
			return CreateEncodedStrings( stringDataWithEncoding, stringDataWithEncoding.Length - 1 );
		}

		/// <summary>
		/// Parses a byte array and creates and returns one or more EncodedStrings
		/// from the byte array. NULL bytes are interpreted as string terminators.
		/// </summary>
		/// <param name="encType">The text encoding of the byte-array string.</param>
		/// <param name="stringData">A byte-array string.</param>
		/// <returns>One or more EncodedStrings created from the byte array.</returns>
		public static ArrayList CreateEncodedStrings( TextEncodingType encType, byte[] stringData )
		{
			return CreateEncodedStrings( encType, stringData, stringData.Length );
		}

		/// <summary>
		/// Parses a byte array and creates and returns one or more EncodedStrings
		/// from the byte array. NULL bytes are interpreted as string terminators.
		/// Strings up to and including maxStrings are created and returned; the remainder
		/// of the byte array (if any) is included as a byte array at the end of the ArrayList.
		/// Thus, the number of items returned may be [maxStrings+1], where the first
		/// [maxStrings] are valid strings, and the last is the array of bytes that
		/// were not converted into EncodedStrings.
		/// </summary>
		/// <param name="stringDataWithEncoding">A byte-array string whose first byte
		/// is a text encoding field.</param>
		/// <param name="maxStrings">The maximum number of strings to create.</param>
		/// <returns>One or more EncodedStrings created from the byte array.</returns>
		public static ArrayList CreateEncodedStrings( byte[] stringDataWithEncoding, int maxStrings )
		{
			TextEncodingType encType = (TextEncodingType) stringDataWithEncoding[0];
			byte[] textBytes = new byte[ stringDataWithEncoding.Length - 1 ];
			Array.Copy( stringDataWithEncoding, 1, textBytes, 0, textBytes.Length );

			ArrayList strings;
			if ( textBytes.Length > 0 )
			{
				strings = CreateStrings( encType, textBytes, maxStrings );
			}
			else
			{
				strings = new ArrayList(1);
				strings.Add( new EncodedString( encType, "" ) );
			}
			
			return strings;
		}

		/// <summary>
		/// Parses a byte array and creates and returns one or more EncodedStrings
		/// from the byte array. NULL bytes are interpreted as string terminators.
		/// Strings up to and including maxStrings are created and returned; the remainder
		/// of the byte array (if any) is included as a byte array at the end of the ArrayList.
		/// Thus, the number of items returned may be [maxStrings+1], where the first
		/// [maxStrings] are valid strings, and the last is the array of bytes that
		/// were not converted into EncodedStrings.
		/// </summary>
		/// <param name="encType">The text encoding of the byte-array string.</param>
		/// <param name="stringData">A byte-array string.</param>
		/// <param name="maxStrings">The maximum number of strings to create.</param>
		/// <returns>One or more EncodedStrings created from the byte array.</returns>
		public static ArrayList CreateEncodedStrings( TextEncodingType encType, byte[] stringData, int maxStrings )
		{
			ArrayList strings = CreateStrings( encType, stringData, maxStrings );
			
			return strings;
		}
		#endregion

		#region Factory method helper methods
		/// <summary>
		/// Parses a byte array and creates and returns one or more EncodedStrings
		/// from the byte array. NULL bytes are interpreted as string terminators.
		/// Strings up to and including maxStrings are created and returned; the remainder
		/// of the byte array (if any) is included as a byte array at the end of the ArrayList.
		/// Thus, the number of items returned may be [maxStrings+1], where the first
		/// [maxStrings] are valid strings, and the last is the array of bytes that
		/// were not converted into EncodedStrings.
		/// </summary>
		/// <param name="encType">The text encoding of the byte-array string.</param>
		/// <param name="stringData">A byte-array string.</param>
		/// <param name="maxStrings">The maximum number of strings to create.</param>
		/// <returns>One or more EncodedStrings created from the byte array.</returns>
		private static ArrayList CreateStrings( TextEncodingType encType, byte[] stringData, int maxStrings )
		{
			ArrayList strings = new ArrayList();
			ArrayList nullIndices;
			int incrementSize;
			if ( encType == TextEncodingType.ISO_8859_1 || 
				encType == TextEncodingType.UTF_8 )
			{
				nullIndices = FindSingleByteNulls( stringData, maxStrings );
				incrementSize = 1;
			}
			else
			{
				nullIndices = FindDoubleByteNulls( stringData, maxStrings );
				incrementSize = 2;
			}

			int start = 0;
			foreach( int nullIndex in nullIndices )
			{
				strings.Add( CreateString( start, nullIndex, stringData, encType ) );

				start = nullIndex + incrementSize;
			}

			if ( strings.Count < maxStrings )
			{
				// Make a string from the remaining bytes
				strings.Add( CreateString( start, stringData.Length, stringData, encType ) );
			}
			else
			{
				// Add the remaining bytes to the end
				byte[] remainingBytes = new byte[ stringData.Length - start ];
				Array.Copy( stringData, start, remainingBytes, 0, remainingBytes.Length );
				strings.Add( remainingBytes );
			}

			return strings;
		}

		/// <summary>
		/// Creates and returns an EncodedString from a portion of a byte array.
		/// </summary>
		/// <param name="startIndex">The index (in the byte array) at which to begin
		/// creating the EncodedString.</param>
		/// <param name="endIndex">The index (in the byte array) at which to finish
		/// creating the EncodedString.</param>
		/// <param name="stringData">The byte array from which to create the EncodedString.</param>
		/// <param name="encType">The TextEncodingType of the byte array.</param>
		/// <returns>An EncodedString from a portion of a byte array.</returns>
		private static EncodedString CreateString(
			int startIndex, int endIndex, byte[] stringData, TextEncodingType encType )
		{
			byte[] stringBytes = new byte[ endIndex - startIndex ];
			Array.Copy( stringData, startIndex, stringBytes, 0, stringBytes.Length );
			return new EncodedString( encType, stringBytes );
		}

		/// <summary>
		/// Returns a sorted ArrayList containing indices of NULL bytes in the
		/// given byte array.
		/// </summary>
		/// <param name="data">The byte array to search.</param>
		/// <param name="maxNulls">The maximum number of NULL bytes to find.</param>
		/// <returns>A sorted ArrayList containing indices of NULL bytes in the
		/// given byte array.</returns>
		private static ArrayList FindSingleByteNulls( byte[] data, int maxNulls )
		{
			ArrayList nulls = new ArrayList();

			for ( int dataItr = 0; dataItr < data.Length && nulls.Count < maxNulls; dataItr++ )
			{
				if ( data[ dataItr ] == 0x0 )
				{
					nulls.Add( dataItr );
				}
			}

			return nulls;
		}

		/// <summary>
		/// Returns a sorted ArrayList containing indices of NULL byte-pairs in the
		/// given byte array.
		/// </summary>
		/// <param name="data">The byte array to search.</param>
		/// <param name="maxNulls">The maximum number of NULL bytes to find.</param>
		/// <returns>A sorted ArrayList containing indices of NULL byte-pairs in the
		/// given byte array.</returns>
		private static ArrayList FindDoubleByteNulls( byte[] data, int maxNulls )
		{
			ArrayList nulls = new ArrayList();

			for ( int dataItr = 0; dataItr < data.Length - 1 && nulls.Count < maxNulls; dataItr += 2 )
			{
				if ( data[ dataItr ] == 0x0 && data[ dataItr + 1 ] == 0x0 )
				{
					nulls.Add( dataItr );
				}
			}

			return nulls;
		}
		#endregion

		#region Properties
		/// <summary>
		/// Gets and sets the encoding of this string.
		/// </summary>
		public TextEncodingType TextEncodingType
		{
			get
			{
				return encodingType;
			}
			set
			{
				encodingType = value;
			}
		}

		/// <summary>
		/// Gets the length of the string (in bytes) according to the current encoding.
		/// </summary>
		public int Size
		{
			get
			{
				return Bytes.Length;
			}
		}

		/// <summary>
		/// Gets the byte-array representation of the string.
		/// </summary>
		public byte[] Bytes
		{
			get
			{
				byte[] bytes = null;

				switch( encodingType )
				{
					case TextEncodingType.ISO_8859_1:
						bytes = this.ISO_8859_1Bytes;
						break;
					case TextEncodingType.UTF_16:
						bytes = this.UTF_16Bytes;
						break;
					case TextEncodingType.UTF_16BE:
						bytes = this.UTF_16BEBytes;
						break;
					case TextEncodingType.UTF_8:
						bytes = this.UTF_8Bytes;
						break;
				}

				return bytes;
			}
		}

		/// <summary>
		/// Gets and sets the String representation of the string.
		/// </summary>
		public string String
		{
			get
			{
				string str = "";

				switch( encodingType )
				{
					case TextEncodingType.ISO_8859_1:
						str = this.ISO_8859_1String;
						break;
					case TextEncodingType.UTF_16:
						str = this.UTF_16String;
						break;
					case TextEncodingType.UTF_16BE:
						str = this.UTF_16BEString;
						break;
					case TextEncodingType.UTF_8:
						str = this.UTF_8String;
						break;
				}

				return str;
			}
			set
			{
				if ( value == null )
				{
					str = "";
				}
				else
				{
					str = value;
				}
			}
		}

		/// <summary>
		/// Gets the termination byte array under the current encoding.
		/// </summary>
		public byte[] Termination
		{
			get
			{
				if ( encodingType == TextEncodingType.UTF_16 || 
					encodingType == TextEncodingType.UTF_16BE ) 
				{
					return new byte[] { 0x00, 0x00 };
				}
				else
				{
					return new byte[] { 0x00 };
				}
			}
		}
		#endregion

		#region Conversions
		/// <summary>
		/// Gets and sets the string as an ISO-8859-1-encoded byte array.
		/// </summary>
		private byte[] ISO_8859_1Bytes
		{
			get
			{
				byte[] stringBytes = new byte[ str.Length ];
				for ( int charItr = 0; charItr < str.Length; charItr++ )
				{
					stringBytes[ charItr ] = (byte) str[ charItr ];
				}
				return stringBytes;
			}
			set
			{
				string str = "";
				foreach ( byte stringByte in value )
				{
					str += (char) stringByte;
				}
				this.String = str;
			}
		}

		/// <summary>
		/// Gets the string as an ISO-8859-1-encoded String.
		/// </summary>
		private string ISO_8859_1String
		{
			get
			{
				string str = "";
				foreach ( byte stringByte in ISO_8859_1Bytes )
				{
					str += (char) stringByte;
				}
				return str;
			}
		}

		/// <summary>
		/// Gets and sets the string as a UTF-16-encoded byte array.
		/// </summary>
		private byte[] UTF_16Bytes
		{
			get
			{
				byte[] bom = utf16Encoding.GetPreamble();
				int length = utf16Encoding.GetByteCount( str ) + bom.Length;
				byte[] bytes = new byte[ length ];
				Array.Copy( bom, 0, bytes, 0, bom.Length );
				utf16Encoding.GetBytes( str, 0, str.Length, bytes, bom.Length );

				return bytes;
			}
			set
			{
				this.String = utf16Encoding.GetString( value );
			}
		}

		/// <summary>
		/// Gets the string as a UTF-16-encoded String.
		/// </summary>
		private string UTF_16String
		{
			get
			{
				return utf16Encoding.GetString( UTF_16Bytes );
			}
		}

		/// <summary>
		/// Gets and sets the string as a big-endian, UTF-16-encoded byte array.
		/// </summary>
		private byte[] UTF_16BEBytes
		{
			get
			{
				return utf16BEEncoding.GetBytes( str );
			}
			set
			{
				this.String = utf16BEEncoding.GetString( value );
			}
		}

		/// <summary>
		/// Gets the string as a big-endian, UTF-16-encoded String.
		/// </summary>
		private string UTF_16BEString
		{
			get
			{
				return utf16BEEncoding.GetString( UTF_16BEBytes );
			}
		}

		/// <summary>
		/// Gets and sets the string as a UTF-8-encoded byte array.
		/// </summary>
		private byte[] UTF_8Bytes
		{
			get
			{
				return utf8Encoding.GetBytes( str );
			}
			set
			{
				this.String = utf8Encoding.GetString( value );
			}
		}

		/// <summary>
		/// Gets the string as a UTF-8-encoded String.
		/// </summary>
		private string UTF_8String
		{
			get
			{
				return utf8Encoding.GetString( UTF_8Bytes );
			}
		}

		/// <summary>
		/// Returns the string value of the given EncodedString.
		/// </summary>
		/// <param name="value">The EncodedString to cast.</param>
		/// <returns>The string value of the given EncodedString.</returns>
		public static explicit operator string( EncodedString value )
		{
			return value.String;
		}
		
		/// <summary>
		/// Returns the byte-array value of the given EncodedString.
		/// </summary>
		/// <param name="value">The EncodedString to cast.</param>
		/// <returns>The byte-array value of the given EncodedString.</returns>
		public static explicit operator byte[]( EncodedString value )
		{
			return value.Bytes;
		}
		#endregion
	}
}
